/**
* File: datatables.responsive.js
* Version: 0.1.2
* Author: Seen Sai Yang
* Info: https://github.com/Comanche/datatables-responsive
*
* Copyright 2013 Seen Sai Yang, all rights reserved.
*
* This source file is free software, under either the GPL v2 license or a
* BSD style license.
*
* This source file is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the license files for details.
*
* You should have received a copy of the GNU General Public License and the
* BSD license along with this program. These licenses are also available at:
* https://raw.github.com/Comanche/datatables-responsive/master/license-gpl2.txt
* https://raw.github.com/Comanche/datatables-responsive/master/license-bsd.txt
*/
'use strict';
/**
* Constructor for responsive datables helper.
*
* This helper class makes datatables responsive to the window size.
*
* The parameter, breakpoints, is an object for each breakpoint key/value pair
* with the following format: { breakpoint_name: pixel_width_at_breakpoint }.
*
* An example is as follows:
*
* {
* tablet: 1024,
* phone: 480
* }
*
* These breakpoint name may be used as possible values for the data-hide
* attribute. The data-hide attribute is optional and may be defined for each
* th element in the table header.
*
* @param {Object|string} tableSelector jQuery wrapped set or selector for
* datatables container element.
* @param {Object} breakpoints Object defining the responsive
* breakpoint for datatables.
*/
function ResponsiveDatatablesHelper(tableSelector, breakpoints) {
if (typeof tableSelector === 'string') {
this.tableElement = $(tableSelector);
} else {
this.tableElement = tableSelector;
}
// State of column indexes and which are shown or hidden.
this.columnIndexes = [];
this.columnsShownIndexes = [];
this.columnsHiddenIndexes = [];
// Index of the th in the header tr that stores where the attribute
// data-class="expand"
// is defined.
this.expandColumn = undefined;
// Stores the break points defined in the table header.
// Each th in the header tr may contain an optional attribute like
// data-hide="phone,tablet"
// These attributes and the breakpoints object will be used to create this
// object.
this.breakpoints = {
/**
* We will be generating data in the following format:
* phone : {
* lowerLimit : undefined,
* upperLimit : 320,
* columnsToHide: []
* },
* tablet: {
* lowerLimit : 320,
* upperLimit : 724,
* columnsToHide: []
* }
*/
};
// Expand icon template
this.expandIconTemplate = '';
// Row template
this.rowTemplate = '
|
';
this.rowLiTemplate = ': ';
// Responsive behavior on/off flag
this.disabled = false;
// Skip next windows width change flag
this.skipNextWindowsWidthChange = false;
// Initialize settings
this.init(breakpoints);
}
/**
* Responsive datatables helper init function. Builds breakpoint limits
* for columns and begins to listen to window resize event.
*
* See constructor for the breakpoints parameter.
*
* @param {Object} breakpoints
*/
ResponsiveDatatablesHelper.prototype.init = function (breakpoints) {
/** Generate breakpoints in the format we need. ***************************/
// First, we need to create a sorted array of the breakpoints given.
var breakpointsSorted = [];
_.each(breakpoints, function (value, key) {
breakpointsSorted.push({
name : key,
upperLimit : value,
columnsToHide: []
});
});
breakpointsSorted = _.sortBy(breakpointsSorted, 'upperLimit');
// Set lower and upper limits for each breakpoint.
var lowerLimit = undefined;
_.each(breakpointsSorted, function (value) {
value.lowerLimit = lowerLimit;
lowerLimit = value.upperLimit;
});
// Add the default breakpoint which shows all (has no upper limit).
breakpointsSorted.push({
name : 'default',
lowerLimit : lowerLimit,
upperLimit : undefined,
columnsToHide: []
});
// Copy the sorted breakpoint array into the breakpoints object using the
// name as the key.
for (var i = 0; i < breakpointsSorted.length; i++) {
this.breakpoints[breakpointsSorted[i].name] = breakpointsSorted[i];
}
/** Create range of possible column indexes *******************************/
// Get all visible column indexes
var columns = this.tableElement.fnSettings().aoColumns;
for (var i = 0; i < columns.length; i++) {
if (columns[i].bVisible) {
this.columnIndexes.push(i)
}
}
// We need the range of possible column indexes to calculate the columns
// to show:
// Columns to show = all columns - columns to hide
var headerElements = $('thead th', this.tableElement);
/** Add columns into breakpoints respectively *****************************/
// Read column headers' attributes and get needed info
_.each(headerElements, function (element, index) {
// Get the column with the attribute data-class="expand" so we know
// where to display the expand icon.
if ($(element).attr('data-class') === 'expand') {
this.expandColumn = index;
}
// The data-hide attribute has the breakpoints that this column
// is associated with.
// If it's defined, get the data-hide attribute and sort this
// column into the appropriate breakpoint's columnsToHide array.
var dataHide = $(element).attr('data-hide');
if (dataHide !== undefined) {
var splitBreakingPoints = dataHide.split(/,\s*/);
_.each(splitBreakingPoints, function (e) {
if (this.breakpoints[e] !== undefined) {
// Translate visible column index to internal column index.
this.breakpoints[e].columnsToHide.push(this.columnIndexes[index]);
}
}, this);
}
}, this);
// Watch the window resize event and response to it.
var that = this;
$(window).bind("resize", function () {
that.respond();
});
};
/**
* Respond window size change. This helps make datatables responsive.
*/
ResponsiveDatatablesHelper.prototype.respond = function () {
if (this.disabled) {
return;
}
// Get new windows width
var newWindowWidth = $(window).width();
var updatedHiddenColumnsCount = 0;
// Loop through breakpoints to see which columns need to be shown/hidden.
var newColumnsToHide = [];
_.each(this.breakpoints, function (element) {
if ((!element.lowerLimit || newWindowWidth > element.lowerLimit) && (!element.upperLimit || newWindowWidth <= element.upperLimit)) {
newColumnsToHide = element.columnsToHide;
}
}, this);
// Find out if a column show/hide should happen.
// Skip column show/hide if this window width change follows immediately
// after a previous column show/hide. This will help prevent a loop.
var columnShowHide = false;
if (!this.skipNextWindowsWidthChange) {
// Check difference in length
if (this.columnsHiddenIndexes.length !== newColumnsToHide.length) {
// Difference in length
columnShowHide = true;
} else {
// Same length but check difference in values
var d1 = _.difference(this.columnsHiddenIndexes, newColumnsToHide).length;
var d2 = _.difference(newColumnsToHide, this.columnsHiddenIndexes).length;
columnShowHide = d1 + d2 > 0;
}
}
if (columnShowHide) {
// Showing/hiding a column at breakpoint may cause a windows width
// change. Let's flag to skip the column show/hide that may be
// caused by the next windows width change.
this.skipNextWindowsWidthChange = true;
this.columnsHiddenIndexes = newColumnsToHide;
this.columnsShownIndexes = _.difference(this.columnIndexes, this.columnsHiddenIndexes);
this.showHideColumns();
updatedHiddenColumnsCount = this.columnsHiddenIndexes.length;
this.skipNextWindowsWidthChange = false;
}
// We don't skip this part.
// If one or more columns have been hidden, add the has-columns-hidden class to table.
// This class will show what state the table is in.
if (this.columnsHiddenIndexes.length) {
this.tableElement.addClass('has-columns-hidden');
var that = this;
// Show details for each row that is tagged with the class .detail-show.
$('tr.detail-show', this.tableElement).each(function (index, element) {
var tr = $(element);
if (tr.next('.row-detail').length === 0) {
ResponsiveDatatablesHelper.prototype.showRowDetail(that, tr);
}
});
} else {
this.tableElement.removeClass('has-columns-hidden');
$('tr.row-detail').remove();
}
};
/**
* Show/hide datatables columns.
*/
ResponsiveDatatablesHelper.prototype.showHideColumns = function () {
// Calculate the columns to show
// Show columns that may have been previously hidden.
for (var i = 0, l = this.columnsShownIndexes.length; i < l; i++) {
this.tableElement.fnSetColumnVis(this.columnsShownIndexes[i], true, false);
}
// Hide columns that may have been previously shown.
for (var i = 0, l = this.columnsHiddenIndexes.length; i < l; i++) {
this.tableElement.fnSetColumnVis(this.columnsHiddenIndexes[i], false, false);
}
// Rebuild details to reflect shown/hidden column changes.
var that = this;
$('tr.row-detail').remove();
if (this.tableElement.hasClass('has-columns-hidden')) {
$('tr.detail-show', this.tableElement).each(function (index, element) {
ResponsiveDatatablesHelper.prototype.showRowDetail(that, $(element));
});
}
};
/**
* Create the expand icon on the column with the data-class="expand" attribute
* defined for it's header.
*
* @param {Object} tr table row object
*/
ResponsiveDatatablesHelper.prototype.createExpandIcon = function (tr) {
if (this.disabled) {
return;
}
// Get the td for tr with the same index as the th in the header tr
// that has the data-class="expand" attribute defined.
var tds = $('td', tr);
if (this.expandColumn !== undefined && this.expandColumn < tds.length) {
var td = $(tds[this.expandColumn]);
// Create expand icon if there isn't one already.
if ($('span.responsiveExpander', td).length == 0) {
td.prepend(this.expandIconTemplate);
// Respond to click event on expander icon.
td.on('click', 'span.responsiveExpander', {responsiveDatatablesHelperInstance: this}, this.showRowDetailEventHandler);
}
}
};
/**
* Show row detail event handler.
*
* This handler is used to handle the click event of the expand icon defined in
* the table row data element.
*
* @param {Object} event jQuery event object
*/
ResponsiveDatatablesHelper.prototype.showRowDetailEventHandler = function (event) {
if (this.disabled) {
return;
}
// Get the parent tr of which this td belongs to.
var tr = $(this).closest('tr');
// Show/hide row details
if (tr.hasClass('detail-show')) {
ResponsiveDatatablesHelper.prototype.hideRowDetail(event.data.responsiveDatatablesHelperInstance, tr);
} else {
ResponsiveDatatablesHelper.prototype.showRowDetail(event.data.responsiveDatatablesHelperInstance, tr);
}
tr.toggleClass('detail-show');
// Prevent click event from bubbling up to higher-level DOM elements.
event.stopPropagation();
};
/**
* Show row details
*
* @param {ResponsiveDatatablesHelper} responsiveDatatablesHelperInstance instance of ResponsiveDatatablesHelper
* @param {Object} tr jQuery wrapped set
*/
ResponsiveDatatablesHelper.prototype.showRowDetail = function (responsiveDatatablesHelperInstance, tr) {
// Get column because we need their titles.
var tableContainer = responsiveDatatablesHelperInstance.tableElement;
var columns = tableContainer.fnSettings().aoColumns;
// Create the new tr.
var newTr = $(responsiveDatatablesHelperInstance.rowTemplate);
// Get the ul that we'll insert li's into.
var ul = $('ul', newTr);
// Loop through hidden columns and create an li for each of them.
_.each(responsiveDatatablesHelperInstance.columnsHiddenIndexes, function (index) {
var li = $(responsiveDatatablesHelperInstance.rowLiTemplate);
$('.columnTitle', li).html(columns[index].sTitle);
li.append(tableContainer.fnGetData(tr[0], index));
ul.append(li);
});
// Create tr colspan attribute
var colspan = responsiveDatatablesHelperInstance.columnIndexes.length - responsiveDatatablesHelperInstance.columnsHiddenIndexes.length;
$('td', newTr).attr('colspan', colspan);
// Append the new tr after the current tr.
tr.after(newTr);
};
/**
* Hide row details
*
* @param {ResponsiveDatatablesHelper} responsiveDatatablesHelperInstance instance of ResponsiveDatatablesHelper
* @param {Object} tr jQuery wrapped set
*/
ResponsiveDatatablesHelper.prototype.hideRowDetail = function (responsiveDatatablesHelperInstance, tr) {
tr.next('.row-detail').remove();
};
/**
* Disable responsive behavior and restores changes made.
*
* @param {Boolean} disable, default is true
*/
ResponsiveDatatablesHelper.prototype.disable = function (disable) {
this.disabled = (disable === undefined) || true;
if (this.disabled) {
// Remove all trs that have row details.
$('tbody tr.row-detail', this.tableElement).remove();
// Remove all trs that are marked to have row details shown.
$('tbody tr', this.tableElement).removeClass('detail-show');
// Remove all expander icons
$('tbody tr span.responsiveExpander', this.tableElement).remove();
this.columnsHiddenIndexes = [];
this.columnsShownIndexes = this.columnIndexes;
this.showHideColumns();
this.tableElement.removeClass('has-columns-hidden');
this.tableElement.off('click', 'span.responsiveExpander', this.showRowDetailEventHandler);
}
}